iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Modern Web

設計系統 - Design System系列 第 9

[Day 9] Design System - React Slots (插槽)

  • 分享至 

  • xImage
  •  

本文同步上傳到筆者的個人部落格,裡面透過 Sandpack 直接編輯程式碼!

什麼是 Slots ?

Slots, 也可以稱為插槽, 就是一個預設的區塊 (placeholder),其顧名思義,可以透過 Slots 在組件中插入任何預先定義的內容,包含其他組件、文字、圖片等等。

為什麼需要 Slots ?

在介紹 Slots 之前,我們先來探討現今 React 組件中最常見的兩種 API 設計方式:配置模式 (Configuration) 和組合模式 (Composition)。再深入瞭解 Slots 是如何為其提供解決方案的。

Image

配置模式 (Configuration)

說到了配置模式,就讓筆者回想到當初在建 <TextInput /> 或是其他一些比較複雜的組件,時常會遇到需要 客製化 的情況。

而那時候的解法都是透過傳入 props 大法,雖然這樣的方法在短期內是有效的,但隨著需求的增加,組件的 API 也會變得越來越繁鎖。

以下筆者將用 <TextInput /> 舉例, 一起來看看其組件的成長史。

首先 v1 版本通常會是和組件的設計指南一樣,有基本的 labelinputhelperText,這時候的組件可能會長這樣

// v1
const TextInput = ({ label, helperText, icon, ...props }) => {
  return (
    <div className="text-input">
      <label>{label}</label>
      <input {...props} />
      <span className="helper-text">{helperText}</span>
      {icon}
    </div>
  );
};

然而,隨著時間的進展,可能有同事提出需要改變 label 的顏色。於是我們引入 labelProps 以便開發者進行客製化:

// v2
const TextInput = ({ label, helperText, icon, labelProps, ...props }) => {
  return (
    <div className="text-input">
      <label {...labelProps}>{label}</label>
      <input {...props} />
      <span className="helper-text">{helperText}</span>
      {icon}
    </div>
  );
};

不久之後,又有同事反應說需要 input 需要加入前綴與後綴,於是我們就加入 prefixsuffix 讓開發者可以自由的客製化 prefixsuffix

// v3
const TextInput = ({ label, helperText, icon, labelProps, prefix, prefixProps, suffix, suffixProps, ...props }) => {
  return (
    <div className="text-input">
      <label {...labelProps}>{label}</label>
      <div>
        {prefix && <span {...prefixProps}>{prefix}</span>}
        <input {...props} />
        {suffix && <span {...suffixProps}>{suffix}</span>}
      </div>
      <span className="helper-text">{helperText}</span>
      {icon}
    </div>
  );
};

隨著設計千奇百怪的需求迎面而來,為了讓組件能夠複用,我們就會需要越來越多的 props 來客製化組件,到最後 <TextInput /> API 可能會非常冗長,讓組件變得難以維護,且混合了各種邏輯。

而組合模式的出現,正是為了嘗試解決這樣的問題。接下來,我們來看看組合模式如何達到這個目的。

組合模式 (Composition)

React 是一個組件化的 UI 框架,而組件化的好處就是可以將一個大的組件拆分成更小的組件,並且可以複用。這就是組合模式的核心思想。

<TextField>
  <TextField.Label />
  <TextField.Input />
  <TextField.HelperText />
  <TextField.Icon />
</TextField>

這種方式提供開發者可以靈活運用組件,自由的定制組件的每一部分,但這引起了另一個問題:一致性,使用者必須按照預期的順序放入子組件,否則可能會導致 Accessibility 問題。

舉例來說 <TextField /> 組件的順序應該是 Label -> Input -> HelperText -> Icon

此時使用者將 Label 放在最後面,並透過 CSS 將 Label 移到 Input 的上方,這樣就會導致 Accessibility 問題,因為對於 Screen Reader 使用者來說,因為他們可能首先接觸到的是 Input,卻還不知道這個輸入框的具體用途。

Slots 的概念

Slots - Web Components

在介紹如何設計 React Slots 之前我們可以透過 Web Components 的 API 來看看 Slots 是如何解決這個問題的。

舉例來說,現在要設計一個 Button 組件,有了 Slots 我們就可以 Slot 的位置,像是按鈕的文字,並且給予一個預設的內容,當有需要客製化的時候,就可以透過 slot 屬性直接來插入內容。

GIF

CodeSandbox Playground

Slots - React

在 React 中,雖然沒有原生 Slots AP,但我們可以透過 children 來達到相同的效果,但是 children 並沒有提供名稱的概念,因此需要透過 props 來定義。

Default Slot

首先,先介紹當只需要定義一個 Slot, 讓我們來看看如何透過 children 來實現 Slots。

const Button = ({ children }) => {
  return <button>{children || 'Default Button'}</button>;
};

這樣的寫法,就可以讓開發者在使用 <Button /> 組件時,可以自由的插入任何內容。

<Button>
  <span>Click Me</span>
</Button>

Named Slots

當需要定義多個 Slots 時,例如 Button 與 Icon, 此時我們可以透過 props 來定義

const Button = ({ icon, content }) => {
  return (
    <button>
      {icon}
      {content || 'Default Button'}
    </button>
  );
};

而這個做法有一個缺點,所有的 Slot 都需要透過 props 來定義。

<Button icon={<Icon />} content={<span>Click Me</span>} />

Slots with createSlots

為了解決上面的問題,可以與組合模式的概念進行結合,並透過遍歷 React Children 的方式找到每個子組件的型別,並且將其放入對應的 Slot 中。

首先使用方式大概會像這樣

<Button>
  <Icon>🖕</Icon>
  <ButtonContent>Click Me!</ButtonContent>
</Button>

而 JSX 會被轉換成以下的結構

{
  type: function Button() {},
  props: {
    children: [
      {
        type: function Icon() {},
        props: {},
      },
      {
        type: function ButtonContent() {},
        props: {
          children: 'Click Me',
        },
      },
    ],
  },
}

再透過這個結構,遍歷 children 來找到每個子組件的型別,並且將其放入對應的 Slot 中。

function ButtonContent(props) {
  return <>{props.children}</>;
}

function Icon(props) {
  return <>{props.children}</>;
}

const Button = (props) => {
  const { children } = props;

  let icon = null;
  let content = null;

  React.Children.forEach(children, (child) => {
    if (!React.isValidElement(child)) return;
    if (child.type === Icon) {
      header = child;
    } else {
      content = child;
    }
  });

  return (
    <button>
      {slot.icon}
      {slot.content || 'Default Button'}
    </button>
  );
};

GIF

CodeSandbox Playground

而從上面的例子中,可以看到我們可以透過 slot 屬性來插入內容,就可以解決 Configuration 模式的痛點,不用再傳入過多的 props 來客製化組件。也可以解決 Composition 模式的痛點,因為開發者可以自由的插入內容,而不用擔心順序的問題。

小結

下一章節我們將介紹如何透過 React 的 Context API 來實現 Slots。

Reference

  1. React-slots

上一篇
[Day 8] Design System - FocusScope 組件 (二)
下一篇
[Day 10] Design System - React Slots (插槽) - 實作
系列文
設計系統 - Design System30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言